<?php

namespace common\components\wechat\components;

use Yii;
use yii\base\Component;
use DOMDocument;
use DOMElement;
use DOMText;

/**
 * 微信SDK操作基类
 *
 * @package common\components\wechat
 */
class BasePayment extends Component {

    /**
     * 微信支付接口基本地址
     */
    const WECHAT_PAYMENT_BASE_URL = 'https://api.mch.weixin.qq.com';
    //
    const CERT_PKCS12_NAME = 'apiclient_cert.p12';
    const CERT_PEM_NAME = 'apiclient_cert.pem';
    const CERT_PEM_KEY = 'apiclient_key.pem';

    /**
     * 代理设置
     * 默认false
     * 如有代理格式：10.206.30.98:8080
     */
    public $proxy = false;

    /**
     * 证书路径
     * 默认false
     * 如有代理格式：10.206.30.98:8080
     */
    public $cert_path = '/data/certs/wechat/';

    /**
     * Access Token更新后事件
     */
    const EVENT_AFTER_ACCESS_TOKEN_UPDATE = 'afterAccessTokenUpdate';

    /**
     * JS API更新后事件
     */
    const EVENT_AFTER_JS_API_TICKET_UPDATE = 'afterJsApiTicketUpdate';

    /**
     * 数据缓存前缀
     * @var string
     */
    public $cachePrefix = 'cache_wechat_sdk';

    /**
     * 返回错误码
     * @var array
     */
    public $lastError;
    //
    private $_transport = null;

    /**
     * 加密XML数据
     * @param string $xml 加密的XML
     * @param string $timestamp 加密时间戳
     * @param string $nonce 加密随机串
     * @return string|bool
     */
    public function encryptXml($xml, $timestamp, $nonce) {
        $errorCode = $this->getMessageCrypt()->encryptMsg($xml, $timestamp, $nonce, $xml);
        if ($errorCode) {
            $this->lastError = [
                'errcode' => $errorCode,
                'errmsg' => 'XML数据加密失败!'
            ];
            return false;
        }
        return $xml;
    }

    /**
     * 解密XML数据
     * @param string $xml 解密的XML
     * @param string $messageSignature 加密签名
     * @param string $timestamp 加密时间戳
     * @param string $nonce 加密随机串
     * @return string|bool
     */
    public function decryptXml($xml, $messageSignature, $timestamp, $nonce) {
        $errorCode = $this->getMessageCrypt()->decryptMsg($messageSignature, $timestamp, $nonce, $xml, $xml);
        if ($errorCode) {
            $this->lastError = [
                'errcode' => $errorCode,
                'errmsg' => 'XML数据解密失败!'
            ];
            return false;
        }
        return $xml;
    }

    /**
     * 创建微信格式的XML
     * @param array $data
     * @param null $charset
     * @return string
     */
    public function xml(array $data, $charset = null) {
        $dom = new DOMDocument('1.0', $charset === null ? Yii::$app->charset : $charset);
        $root = new DOMElement('xml');
        $dom->appendChild($root);
        $this->buildXml($root, $data);
        $xml = $dom->saveXML();
        return trim(substr($xml, strpos($xml, '?>') + 2));
    }

    /**
     * @var string the name of the elements that represent the array elements with numeric keys.
     */
    public $itemTag = 'item';

    /**
     * @see yii\web\XmlResponseFormatter::buildXml()
     */
    protected function buildXml($element, $data) {
        if (is_object($data)) {
            $child = new DOMElement(StringHelper::basename(get_class($data)));
            $element->appendChild($child);
            if ($data instanceof Arrayable) {
                $this->buildXml($child, $data->toArray());
            } else {
                $array = [];
                foreach ($data as $name => $value) {
                    $array[$name] = $value;
                }
                $this->buildXml($child, $array);
            }
        } elseif (is_array($data)) {
            foreach ($data as $name => $value) {
                if (is_int($name) && is_object($value)) {
                    $this->buildXml($element, $value);
                } elseif (is_array($value) || is_object($value)) {
                    $child = new DOMElement(is_int($name) ? $this->itemTag : $name);
                    $element->appendChild($child);
                    $this->buildXml($child, $value);
                } else {
                    $child = new DOMElement(is_int($name) ? $this->itemTag : $name);
                    $element->appendChild($child);
                    $child->appendChild(new DOMText((string) $value));
                }
            }
        } else {
            $element->appendChild(new DOMText((string) $data));
        }
    }

    /**
     * Api url 组装
     * @param $url
     * @param array $options
     * @return string
     */
    protected function httpBuildQuery($url, array $options) {
        if (!empty($options)) {
            $url .= (stripos($url, '?') === null ? '&' : '?') . http_build_query($options);
        }
        return $url;
    }

    /**
     * Http Get 请求
     * @param $url
     * @param array $options
     * @return mixed
     */
    public function httpGet($url, array $options = []) {
        Yii::info([
            'url' => $url,
            'options' => $options
        ], __METHOD__);
        return $this->parseHttpRequest(function($url) {
            return $this->http($url);
        }, $this->httpBuildQuery($url, $options));
    }

    /**
     * Http Post 请求
     * @param $url
     * @param array $postOptions
     * @param array $options
     * @return mixed
     */
    public function httpPost($url, array $postOptions, array $options = []) {
        Yii::info([
            'url' => $url,
            'postOptions' => $postOptions,
            'options' => $options
        ], __METHOD__);
        return $this->parseHttpRequest(function($url, $postOptions) {
            return $this->http($url, [
                CURLOPT_POST => true,
                CURLOPT_POSTFIELDS => $postOptions
            ]);
        }, $this->httpBuildQuery($url, $options), $postOptions);
    }

    /**
     * Http Raw数据 Post 请求
     * @param $url
     * @param $postOptions
     * @param array $options
     * @return mixed
     */
    public function httpRaw($url, $postOptions, array $options = []) {
        Yii::info([
            'url' => $url,
            'postOptions' => $postOptions,
            'options' => $options
        ], __METHOD__);
        return $this->parseHttpRequest(function($url, $postOptions) {
            return $this->http($url, [
                CURLOPT_POST => true,
                CURLOPT_POSTFIELDS => is_array($postOptions) ? json_encode($postOptions, JSON_UNESCAPED_UNICODE) : $postOptions
            ]);
        }, $this->httpBuildQuery($url, $options), $postOptions);
    }

    /**
     * Http基础库 使用该库请求微信服务器
     * @param $url
     * @param array $options
     * @return bool|mixed
     */
    protected function http($url, $options = []) {
        if (stripos($url, 'http://') === false && stripos($url, 'https://') === false) {
            $url = self::WECHAT_PAYMENT_BASE_URL . $url;
        }
        $_options = [
            CURLOPT_URL => $url,
            CURLOPT_TIMEOUT => 30,
            CURLOPT_CONNECTTIMEOUT => 30,
            CURLOPT_RETURNTRANSFER => true,
        ] + (stripos($url, "https://") !== false ? [
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false,
            CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1 // 微信官方屏蔽了ssl2和ssl3, 启用更高级的ssl
        ] : []) + $options;

        $curl = curl_init();
        curl_setopt_array($curl, $_options);
        $content = curl_exec($curl);
        $status = curl_getinfo($curl);
        curl_close($curl);

        if (isset($status['http_code']) && $status['http_code'] == 200) {
            return $content;
        }
        Yii::error([
            'result' => $content,
            'status' => $status
        ], __METHOD__);
        return false;
    }

    /**
     * 上传文件请使用该类来解决curl版本兼容问题
     * @param $filePath
     * @return \CURLFile|string
     */
    protected function uploadFile($filePath) {
        // php 5.5将抛弃@写法,引用CURLFile类来实现
        // @see http://segmentfault.com/a/1190000000725185
        return class_exists('\CURLFile') ? new \CURLFile($filePath) : '@' . $filePath;
    }

    /**
     * 生成签名
     * @param type $params
     * @param type $key
     * @return type
     */
    public static function generateSignature($params, $key) {
        $array = array_filter($params);
        ksort($array);

        $str = '';
        foreach ($array as $k => $v) {
            $str .= "$k=$v&";
        }
        $str .= 'key=' . $key;

//        $str = urldecode(http_build_query($array));
//        $str .= '&key=' . $key;

        Yii::error($str);
        return strtoupper(md5($str));
    }

    public function checkSignature($data) {
        $formatter = new HttpDataFormatter();
        $formatter->partner_key = $this->partner_key;
        return $formatter->checkSignature($data);
    }

    /**
     * @param array|\BaseTransport $transport
     * @throws InvalidConfigException on invalid argument.
     */
    public function setTransport($transport) {
        if (!is_array($transport) && !is_object($transport)) {
            throw new InvalidConfigException('"' . get_class($this) . '::transport" should be either object or array, "' . gettype($transport) . '" given.');
        }
        $this->_transport = array_merge($transport, [
            'appid' => $this->appid,
            'mch_id' => $this->mch_id,
        ]);
    }

    /**
     * @return array|\BaseTransport
     */
    public function getTransport() {
        if (!is_object($this->_transport)) {
            $this->_transport = $this->createTransport($this->_transport);
        }
        return $this->_transport;
    }

    /**
     * Creates Swift library object, from given array configuration.
     * @param array $config object configuration
     * @return Object created object
     * @throws \yii\base\InvalidConfigException on invalid configuration.
     */
    protected function createTransport(array $config) {
        if (!isset($config['class'])) {
            throw new InvalidConfigException('Object configuration must be an array containing a "class" element.');
        }
        return Yii::createObject($config);
    }

    public function curl_post_ssl($url, $vars, $second = 30, $aHeader = array()) {
        if (stripos($url, 'http://') === false && stripos($url, 'https://') === false) {
            $url = self::WECHAT_PAYMENT_BASE_URL . $url;
        }

        $ch = curl_init();

        //超时时间
        curl_setopt($ch, CURLOPT_TIMEOUT, $second);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

        //这里设置代理，如果有的话
        if ($this->proxy !== false) {
            list($proxy, $port) = explode(':', $this->proxy);
            curl_setopt($ch, CURLOPT_PROXY, $proxy);
            curl_setopt($ch, CURLOPT_PROXYPORT, $port);
        }
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);


        $certPath = Yii::getAlias('@common') . $this->cert_path;
        //以下两种方式需选择一种
        //第一种方法，cert 与 key 分别属于两个.pem文件
        //默认格式为PEM，可以注释
        curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
        curl_setopt($ch, CURLOPT_SSLCERT, $certPath . self::CERT_PEM_NAME);
        //默认格式为PEM，可以注释
        curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
        curl_setopt($ch, CURLOPT_SSLKEY, $certPath . self::CERT_PEM_KEY);
        //第二种方式，两个文件合成一个.pem文件
        //curl_setopt($ch, CURLOPT_SSLCERT, $certPath . self::CERT_PKCS12_NAME);

        if (count($aHeader) >= 1) {
            curl_setopt($ch, CURLOPT_HTTPHEADER, $aHeader);
        }

        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $vars);
        $data = curl_exec($ch);
        if ($data) {
            curl_close($ch);
            return $data;
        } else {
            $error = curl_error($ch);
            Yii::error("Call Faild! ErrorCode:$error\n");
            curl_close($ch);
            return $error;
        }
    }

    /**
     * 解析微信服务器请求的xml数据, 如果是加密数据直接自动解密
     * @param string $xml 微信请求的XML信息主体, 默认取$_GET数据
     * @param string $messageSignature 加密签名, 默认取$_GET数据
     * @param string $timestamp 加密时间戳, 默认取$_GET数据
     * @param string $nonce 加密随机串, 默认取$_GET数据
     * @param string $encryptType 加密类型, 默认取$_GET数据
     * @return array
     */
    public function parseRequestXml($xml = null, $messageSignature = null, $timestamp = null, $nonce = null, $encryptType = null) {
        if ($xml === null) {
            $xml = Yii::$app->request->getRawBody();
        }
        if (empty($xml)) {
            return [];
        }
        if ($messageSignature === null) {
            $messageSignature = Yii::$app->request->get('msg_signature');
        }
        if ($encryptType === null) {
            $encryptType = Yii::$app->request->get('encrypt_type');
        }
        if ($messageSignature !== null && $encryptType == 'aes') {
            // 自动解密
            if ($timestamp === null) {
                $timestamp = Yii::$app->request->get('timestamp');
            }
            if ($nonce === null) {
                $nonce = Yii::$app->request->get('nonce');
            }
            $xml = $this->decryptXml($xml, $messageSignature, $timestamp, $nonce);
            if ($xml === false) {
                return [];
            }
        }
        return (array) simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA);
    }

    /**
     * @inheritdoc
     * @param bool $force 是否强制获取access_token, 该设置会在access_token使用错误时, 是否再获取一次access_token并再重新提交请求
     */
    public function parseHttpRequest(callable $callable, $url, $postOptions = null) {
        $result = call_user_func_array($callable, [$url, $postOptions]);
        if (isset($result['errcode']) && $result['errcode']) {
            $this->lastError = $result;
            Yii::warning([
                'url' => $url,
                'result' => $result,
                'postOptions' => $postOptions
            ], __METHOD__);
        }
        return $result;
    }

}
